#!/usr/bin/env python
# -*-  coding: utf-8 -*-
#   
#   Copyright © 2008, 2009, 2010 Jan Svec <honza.svec@gmail.com> and Filip Jurcicek <filip.jurcicek@gmail.com>
#   
#   This file is part of Gmail Backup.
#
#   Gmail Backup is free software: you can redistribute it and/or modify it
#   under the terms of the GNU General Public License as published by the Free
#   Software Foundation, either version 3 of the License, or (at your option)
#   any later version.
#
#   Gmail Backup is distributed in the hope that it will be useful, but WITHOUT
#   ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
#   FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
#   more details.
#
#   You should have received a copy of the GNU General Public License along
#   with Gmail Backup.  If not, see <http://www.gnu.org/licenses/
#
#   See LICENSE file for license details
#
#   With changes by Jesper Hertel <jesper.hertel@gmail.com> (JH).

import threading
import inspect
import ctypes

_submoduleListForVersioning = []
import versioning; _revisionInfo = versioning.getRevisionInfoFromTagsAndSubModules('$Revision: 117 $', '$Date: 2013-08-24 21:34:40 +0000 (Sat, 24 Aug 2013) $', _submoduleListForVersioning)

class ThreadDoesNotExistError(threading.ThreadError):
    pass

class ThreadIsNotActiveError(threading.ThreadError):
    pass

class InterruptableThread(threading.Thread):
    
    @classmethod
    def _async_raise(cls, threadId, exceptionType):
        '''
        Raise the given exception *type* exceptionType in the
        thread with the given ID threadId.
        
        :param cls:
            Class InterruptableThread.
        :param threadId:
            The ID of the thread to raise the exception in.
        :param exceptionType:
            The exception *type* that should be raised.
            This must be a type and not an instance due to a 
            bug in ctypes.pythonapi.PyThreadState_SetAsyncExc().
        '''
        # The original code in this method seems to come from 
        # http://code.activestate.com/recipes/496960-thread2-killable-threads/
        # and/or http://tomerfiliba.com/recipes/Thread2/.
        if not inspect.isclass(exceptionType):
            raise TypeError("Only types, not instances, can be raised by InterruptableThread._async_raise()")
        result = ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(threadId), ctypes.py_object(exceptionType))
        if result == 0:
            raise ThreadDoesNotExistError()
        elif result > 1:
            # """if it returns a number greater than one, you're in trouble, 
            # and you should call it again with exc=NULL to revert the effect"""
            #
            # "Thomas Heller 
            # Small bug in the recipe. The second call to PyThreadState_SetAsyncEx must use 
            # 'None' as second parameter, otherwise it will not work correctly on 64-bit plaforms:
            # ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, None)
            # 'None' will be passed as pointer parameter, '0' would be passed as integer."
            # (http://code.activestate.com/recipes/496960-thread2-killable-threads/#c1)
            ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(threadId), None)
            raise SystemError("PyThreadState_SetAsyncExc() failed")


    def getThreadId(self):
        '''
        Return the threadId of the thread, if any, or None if the thread 
        was not found among the active threads.
        
        JH: Original code seems to come from http://tomerfiliba.com/recipes/Thread2/.
        '''
        if not self.isAlive():
            raise ThreadIsNotActiveError()

        else:
            if hasattr(self, "_threadId"):
                # We have it cached.
                foundThreadId = self._threadId
            else:
                # Not cached.
                for threadId, threadObject in threading._active.items():
                    if threadObject is self:
                        foundThreadId = threadId
                        break
                else:  # Yes, this else belongs to the for loop.
                    foundThreadId = None
                self._threadId = foundThreadId
    
            return foundThreadId 


    def raiseExceptionType(self, exceptionType):
        '''
        Raise an exception of the given exception type in the thread.
        
        @param exceptionType: 
            The exception *type* to raise.
            Note that it needs to be a type, not an instance,
            because of a bug in PyThreadState_SetAsyncExc().
            See http://tomerfiliba.com/recipes/Thread2/.
        
        @raise 
            ThreadDoesNotExistError() – when the InterruptableThread does not exist.
            SystemError() – when PyThreadState_SetAsyncExc() fails badly.
        '''
        threadId = self.getThreadId()
        if (threadId is not None):
            self._async_raise(threadId, exceptionType)

    def stopThread(self):
        # must raise the KeyboardInterrupt *type*, instead of a KeyboardInterrupt() instance
        # due to a bug in PyThreadState_SetAsyncExc()
        self.raiseExceptionType(KeyboardInterrupt) 

    def terminate(self):
        # must raise the SystemExit *type*, instead of a SystemExit() instance
        # due to a bug in PyThreadState_SetAsyncExc()
        self.raiseExceptionType(SystemExit)

